跳到主要内容

MySQL 中的 MVCC 与幻读

幻读基础概念

什么是幻读?

幻读(Phantom Read) 是指在同一个事务中,前后两次执行相同的范围查询,得到不同数量记录的现象。就像看到了"幻影"一样,原本不存在的记录突然出现了。

幻读的特征:

  • 发生在范围查询中(如 WHERE age > 18
  • 主要是新增记录导致的
  • 可重复读隔离级别下仍可能发生

幻读与其他读异常的区别

MVCC 如何处理幻读

快照读 vs 当前读

MySQL 通过两种不同的读取方式来处理幻读问题:

快照读的幻读解决方案

REPEATABLE READ 隔离级别下,快照读通过 MVCC 完全避免幻读:

当前读的幻读解决方案

当前读通过 Next-Key Lock(记录锁 + 间隙锁)来避免幻读:

特殊的幻读场景

可重复读下的"半幻读"现象

即使在 REPEATABLE READ 隔离级别下,某些情况下仍可能出现类似幻读的现象:

原因分析:

  • 第一次 SELECT(快照读):使用 ReadView,看不到 T2 插入的记录
  • UPDATE 操作(当前读):能看到最新数据并执行修改
  • 第二次 SELECT(快照读):能看到自己修改的记录(trx_id 变成了自己的事务 ID)

解决方案对比

实际应用场景

电商库存管理

-- 错误的库存扣减方式(可能出现幻读)
BEGIN;
SELECT stock FROM products WHERE id = 1001; -- 假设返回 stock = 10
-- 其他事务可能插入/修改了库存记录
UPDATE products SET stock = stock - 1 WHERE id = 1001; -- 基于过时的数据更新
COMMIT;

-- 正确的库存扣减方式(使用当前读)
BEGIN;
SELECT stock FROM products WHERE id = 1001 FOR UPDATE; -- 当前读 + 加锁
UPDATE products SET stock = stock - 1 WHERE id = 1001;
COMMIT;

订单系统统计

Go 语言实现示例

// 库存扣减 - 防止幻读的正确实现
func DeductStock(db *sql.DB, productID int, quantity int) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()

// 使用当前读获取最新库存,同时加锁防止并发修改
var currentStock int
err = tx.QueryRow(`
SELECT stock FROM products
WHERE id = ? FOR UPDATE
`, productID).Scan(&currentStock)
if err != nil {
return err
}

// 检查库存是否充足
if currentStock < quantity {
return errors.New("insufficient stock")
}

// 扣减库存
_, err = tx.Exec(`
UPDATE products
SET stock = stock - ?, updated_at = NOW()
WHERE id = ?
`, quantity, productID)
if err != nil {
return err
}

return tx.Commit()
}

// 订单统计 - 使用快照读保证一致性
func GetOrderStatistics(db *sql.DB, startTime, endTime time.Time) (*OrderStats, error) {
// 在可重复读隔离级别下,所有查询使用相同的 ReadView
tx, err := db.Begin()
if err != nil {
return nil, err
}
defer tx.Rollback()

stats := &OrderStats{}

// 查询总订单数
err = tx.QueryRow(`
SELECT COUNT(*) FROM orders
WHERE created_at BETWEEN ? AND ?
`, startTime, endTime).Scan(&stats.TotalOrders)
if err != nil {
return nil, err
}

// 查询总金额 - 使用相同的 ReadView,保证数据一致性
err = tx.QueryRow(`
SELECT COALESCE(SUM(amount), 0) FROM orders
WHERE created_at BETWEEN ? AND ?
`, startTime, endTime).Scan(&stats.TotalAmount)
if err != nil {
return nil, err
}

tx.Commit()
return stats, nil
}

性能优化建议

减少锁冲突的策略

监控和调试

-- 查看当前锁等待情况
SELECT * FROM information_schema.INNODB_LOCK_WAITS;

-- 查看事务状态
SELECT * FROM information_schema.INNODB_TRX;

-- 分析慢查询中的锁等待
SHOW PROCESSLIST;

-- 查看间隙锁信息
SELECT * FROM performance_schema.data_locks
WHERE LOCK_TYPE = 'RECORD' AND LOCK_MODE LIKE '%GAP%';

References